/**
 * Copyright 2019 Huawei Technologies Co.,Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License.  You may obtain a copy of the
 * License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.obs.services.internal.utils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy.Type;
import java.net.Socket;
import java.net.SocketException;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import com.obs.log.ILogger;
import com.obs.log.LoggerBuilder;
import com.obs.services.internal.Constants;
import com.obs.services.internal.Constants.CommonHeaders;
import com.obs.services.internal.ObsConstraint;
import com.obs.services.internal.ObsProperties;
import com.obs.services.internal.RestStorageService;
import com.obs.services.internal.ServiceException;
import com.obs.services.model.HttpProtocolTypeEnum;

import okhttp3.Authenticator;
import okhttp3.ConnectionPool;
import okhttp3.Credentials;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;

public class RestUtils
{
    
    private static final ILogger log = LoggerBuilder.getLogger(RestUtils.class);
    
    private static Pattern chinesePattern = Pattern.compile("[\u4e00-\u9fa5]");
    
    private static final HostnameVerifier ALLOW_ALL_HOSTNAME = new HostnameVerifier()
    {
        
        @Override
        public boolean verify(String arg0, SSLSession arg1)
        {
            return true;
        }
    }; 
    
    
    private static final X509TrustManager TRUST_ALL_MANAGER = new X509TrustManager()
        {
            
            @Override
            public X509Certificate[] getAcceptedIssuers()
            {
                return new X509Certificate[]{};
            }
            
            @Override
            public void checkServerTrusted(X509Certificate[] arg0, String arg1)
                throws CertificateException
            {
            }
            
            @Override
            public void checkClientTrusted(X509Certificate[] arg0, String arg1)
                throws CertificateException
            {
            }
    };
    
    public static String uriEncode(CharSequence input, boolean chineseOnly) throws ServiceException
    {
        
        StringBuilder result = new StringBuilder();
        try{
            
            if(chineseOnly){
                for (int i = 0; i < input.length(); i++){
                    char ch = input.charAt(i);
                    String s = Character.toString(ch);
                    Matcher m = chinesePattern.matcher(s);
                    if(m != null && m.find()){
                        result.append(URLEncoder.encode(s, Constants.DEFAULT_ENCODING));
                    }else{
                        result.append(ch);   
                    }
                }
            }else{
                for (int i = 0; i < input.length(); i++)
                {
                    char ch = input.charAt(i);
                    if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '~'
                        || ch == '.')
                    {
                        result.append(ch);
                    }
                    else if (ch == '/')
                    {
                        result.append("%2F");
                    }
                    else
                    {
                        result.append(URLEncoder.encode(Character.toString(ch), Constants.DEFAULT_ENCODING));
                    }
                }
            }
        }catch (UnsupportedEncodingException e)
        {
            throw new ServiceException("Unable to encode input: " + input);
        }
        return result.toString();
    }
    
    public static String encodeUrlString(String path)
        throws ServiceException
    {
        try
        {
            return URLEncoder.encode(path, Constants.DEFAULT_ENCODING)
                .replaceAll("\\+", "%20") //Web browsers do not always handle '+' characters well, use the well-supported '%20' instead.
                .replaceAll("%7E", "~")
                .replaceAll("\\*", "%2A");
        }
        catch (UnsupportedEncodingException uee)
        {
            throw new ServiceException("Unable to encode path: " + path, uee);
        }
    }
    
    public static String encodeUrlPath(String path, String delimiter)
        throws ServiceException
    {
        StringBuilder result = new StringBuilder();
        String tokens[] = path.split(delimiter);
        for (int i = 0; i < tokens.length; i++)
        {
            result.append(encodeUrlString(tokens[i]));
            if (i < tokens.length - 1)
            {
                result.append(delimiter);
            }
        }
        return result.toString();
    }
    
    private static SSLContext createSSLContext(KeyManager[] km, TrustManager[] tm, String provider) throws Exception{
        SSLContext sslContext;
        try{
            sslContext =SSLContext.getInstance("TLSv1.2", provider); 
        }catch (Exception e) {
            try{
                sslContext = SSLContext.getInstance("TLSv1.1", provider); 
            }catch (Exception ex) {
                try{
                    sslContext = SSLContext.getInstance("TLSv1.0", provider); 
                }catch (Exception exx) {
                    sslContext = SSLContext.getInstance("TLS", provider);
                }
            }
        }
        sslContext.init(km, tm, new SecureRandom());
        return sslContext;
    }
    
    private static SSLContext createSSLContext(KeyManager[] km, TrustManager[] tm) throws Exception{
        SSLContext sslContext;
        try{
            sslContext =SSLContext.getInstance("TLSv1.2"); 
        }catch (Exception e) {
            try{
                sslContext = SSLContext.getInstance("TLSv1.1"); 
            }catch (Exception ex) {
                try{
                    sslContext = SSLContext.getInstance("TLSv1.0"); 
                }catch (Exception exx) {
                    sslContext = SSLContext.getInstance("TLS");
                }
            }
        }
        sslContext.init(km, tm, new SecureRandom());
        return sslContext;
    }
    
    private static class WrapperedSocketFactory extends SocketFactory{
    	
    	private SocketFactory delegate;
    	private int socketReadWriteBufferSize;
    	
    	WrapperedSocketFactory(SocketFactory delegate, int socketReadWriteBufferSize) {
    		this.delegate = delegate;
    		this.socketReadWriteBufferSize = socketReadWriteBufferSize;
		}
    	
    	private Socket doWrap(Socket s) throws SocketException {
    		if(s != null) {
    			if(socketReadWriteBufferSize > 0) {
    				s.setReceiveBufferSize(socketReadWriteBufferSize);
    				s.setReceiveBufferSize(socketReadWriteBufferSize);
    			}
    			s.setTcpNoDelay(true);
    		}
    		return s;
    	}
    	
    	@Override
		public Socket createSocket() throws IOException, UnknownHostException {
			return this.doWrap(this.delegate.createSocket());
		}

		@Override
		public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
			return this.doWrap(this.delegate.createSocket(host, port));
		}

		@Override
		public Socket createSocket(InetAddress host, int port) throws IOException {
			return this.doWrap(this.delegate.createSocket(host, port));
		}

		@Override
		public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
				throws IOException, UnknownHostException {
			return this.doWrap(this.delegate.createSocket(host, port, localHost, localPort));
		}

		@Override
		public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
				throws IOException {
			return this.doWrap(this.delegate.createSocket(address, port, localAddress, localPort));
		}
    	
    }
    
    private static class WrapperedSSLSocketFactory extends SSLSocketFactory{
    	
    	private SSLSocketFactory delegate;
    	private int socketReadWriteBufferSize;
    	
    	WrapperedSSLSocketFactory(SSLSocketFactory delegate, int socketReadWriteBufferSize){
    		this.delegate = delegate;
    		this.socketReadWriteBufferSize = socketReadWriteBufferSize;
    	}
    	
    	private Socket doWrap(Socket s) throws SocketException {
    		if(s != null) {
    			if(socketReadWriteBufferSize > 0) {
    				s.setReceiveBufferSize(socketReadWriteBufferSize);
    				s.setReceiveBufferSize(socketReadWriteBufferSize);
    			}
    			s.setTcpNoDelay(true);
    		}
    		return s;
    	}
    	
    	@Override
		public Socket createSocket() throws IOException, UnknownHostException {
			return this.doWrap(this.delegate.createSocket());
		}
    	
		@Override
		public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
			return doWrap(delegate.createSocket(s, host, port, autoClose));
		}

		@Override
		public String[] getDefaultCipherSuites() {
			return delegate.getDefaultCipherSuites();
		}

		@Override
		public String[] getSupportedCipherSuites() {
			return delegate.getSupportedCipherSuites();
		}

		@Override
		public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
			return doWrap(this.delegate.createSocket(host, port));
		}

		@Override
		public Socket createSocket(InetAddress host, int port) throws IOException {
			return doWrap(delegate.createSocket(host, port));
		}

		@Override
		public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
				throws IOException, UnknownHostException {
			return doWrap(delegate.createSocket(host, port, localHost, localPort));
		}

		@Override
		public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
				throws IOException {
			return doWrap(delegate.createSocket(address, port, localAddress, localPort));
		}
    	
    }
    
    public static OkHttpClient.Builder initHttpClientBuilder(final RestStorageService restStorageService, ObsProperties obsProperties, 
    		KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory, Dispatcher httpDispatcher)
    {
        
        List<Protocol> protocols = new ArrayList<Protocol>(2);
        protocols.add(Protocol.HTTP_1_1);
        
        if(HttpProtocolTypeEnum.getValueFromCode(obsProperties.getStringProperty(ObsConstraint.HTTP_PROTOCOL, HttpProtocolTypeEnum.HTTP1_1.getCode())) == HttpProtocolTypeEnum.HTTP2_0) {
        	 protocols.add(Protocol.HTTP_2);
        }
        
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        
        if(httpDispatcher == null) {
        	 int maxConnections = obsProperties.getIntProperty(ObsConstraint.HTTP_MAX_CONNECT, ObsConstraint.HTTP_MAX_CONNECT_VALUE);
        	 httpDispatcher = new Dispatcher();
        	 httpDispatcher.setMaxRequests(maxConnections);
        	 httpDispatcher.setMaxRequestsPerHost(maxConnections);
        	 builder.dispatcher(httpDispatcher);
        }else {
        	try {
        		Method m = builder.getClass().getMethod("dispatcher", httpDispatcher.getClass());
        		m.invoke(builder, httpDispatcher);
        	}catch (Exception e) {
        		try {
        			Class<?> c = Class.forName("okhttp3.AbsDispatcher");
        			Method m = builder.getClass().getMethod("dispatcher", c);
        			m.invoke(builder, httpDispatcher);
        		}catch (Exception ex) {
        			throw new RuntimeException(ex);
				}
			}
        }
        
        ConnectionPool pool = new ConnectionPool(obsProperties.getIntProperty(ObsConstraint.HTTP_MAX_IDLE_CONNECTIONS, ObsConstraint.DEFAULT_MAX_IDLE_CONNECTIONS), 
        		obsProperties.getIntProperty(ObsConstraint.HTTP_IDLE_CONNECTION_TIME, ObsConstraint.DEFAULT_IDLE_CONNECTION_TIME), TimeUnit.MILLISECONDS);
        
        builder.protocols(protocols).followRedirects(false).followSslRedirects(false).retryOnConnectionFailure(false).cache(null).connectTimeout(obsProperties.getIntProperty(ObsConstraint.HTTP_CONNECT_TIMEOUT, ObsConstraint.HTTP_CONNECT_TIMEOUT_VALUE), TimeUnit.MILLISECONDS)
        .writeTimeout(obsProperties.getIntProperty(ObsConstraint.HTTP_SOCKET_TIMEOUT, ObsConstraint.HTTP_SOCKET_TIMEOUT_VALUE), TimeUnit.MILLISECONDS)
        .readTimeout(obsProperties.getIntProperty(ObsConstraint.HTTP_SOCKET_TIMEOUT, ObsConstraint.HTTP_SOCKET_TIMEOUT_VALUE), TimeUnit.MILLISECONDS)
        .connectionPool(pool).hostnameVerifier(obsProperties.getBoolProperty(ObsConstraint.HTTP_STRICT_HOSTNAME_VERIFICATION, false) ? HttpsURLConnection.getDefaultHostnameVerifier(): ALLOW_ALL_HOSTNAME);
        
        int socketReadBufferSize = obsProperties.getIntProperty(ObsConstraint.SOCKET_READ_BUFFER_SIZE, -1);
        int socketWriteBufferSize = obsProperties.getIntProperty(ObsConstraint.SOCKET_WRITE_BUFFER_SIZE, -1);
        
        int socketReadWriteBufferSize = Math.max(socketReadBufferSize, socketWriteBufferSize);
        
        builder.socketFactory(new WrapperedSocketFactory(SocketFactory.getDefault(), socketReadWriteBufferSize));
        
        try{
            KeyManager[] km = null;
            X509TrustManager trustManager;
            TrustManager[] tm;
            
            if(obsProperties.getBoolProperty(ObsConstraint.VALIDATE_CERTIFICATE, false)){
                km = keyManagerFactory == null ? null : keyManagerFactory.getKeyManagers();
                if(trustManagerFactory == null || trustManagerFactory.getTrustManagers().length < 1){
                    trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                    trustManagerFactory.init((KeyStore)null);
                }
                tm = trustManagerFactory.getTrustManagers();
                trustManager = (X509TrustManager)tm[0];
            }else{
                trustManager = TRUST_ALL_MANAGER;
                tm = new TrustManager[]{trustManager};
            }
            String provider = obsProperties.getStringProperty(ObsConstraint.SSL_PROVIDER, "");
            SSLContext sslContext = null;
            if(ServiceUtils.isValid(provider)) {
            	try {
            		sslContext = createSSLContext(km, tm, provider);
            	}catch (Exception e) {
                	if(log.isErrorEnabled()) {
                		log.error("Exception happened in create ssl context with provider" + provider, e);
                	}
                }
            }
            if(sslContext == null) {
            	sslContext =  createSSLContext(km, tm);
            }
            SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
            builder.sslSocketFactory(new WrapperedSSLSocketFactory(sslSocketFactory, socketReadWriteBufferSize), trustManager);
        }catch (Exception e) {
        	if(log.isErrorEnabled()) {
        		log.error("Exception happened in HttpClient.configSSL,and e = " + e);
        	}
        }
        
        return builder;
    }
    
    
    public static void initHttpProxy(OkHttpClient.Builder builder,
        String proxyHostAddress, int proxyPort, final String proxyUser, final String proxyPassword, String proxyDomain, String proxyWorkstation)
    {
        if (proxyHostAddress != null && proxyPort != -1)
        {
            if (log.isInfoEnabled())
            {
                log.info("Using Proxy: " + proxyHostAddress + ":" + proxyPort);
            }
            builder.proxy(new java.net.Proxy(Type.HTTP, new InetSocketAddress(proxyHostAddress, proxyPort)));
            
            if (proxyUser != null && !proxyUser.trim().equals(""))
            {
                Authenticator proxyAuthenticator = new Authenticator() {
                    @Override public Request authenticate(Route route, Response response) throws IOException {
                         String credential = Credentials.basic(proxyUser, proxyPassword);
                         return response.request().newBuilder()
                             .header(CommonHeaders.PROXY_AUTHORIZATION, credential)
                             .build();
                    }
                };
                builder.proxyAuthenticator(proxyAuthenticator);
            }
        }
    }
}
